閱讀本篇文章前,仔細想想看
- 如何宣告介面(Interface)?
- 介面跟型別(Type)在語法上的差別與規則會是什麼?(筆者目前還沒講概念上的差別,讀者先回想語法層面就夠了)
如果還沒理解完畢的話,可以先翻看前一篇文章喔!
今天要來談談 TypeScript Interface 可以很彈性地編制與擴展的特性與更多可以在介面上面做的事情。
正文開始!
介面的概念很熱門的主要原因是 —— 它可以被組來組去,也可以被延伸(Extend)。
在 TypeScript 通常會聽到 Interface Extension 或 Interface Inheritance,這都是可以的說法,不過筆者偏向於前者,畢竟後續要介紹的另一個關鍵字叫做 extends
。
我們舉一個讀者之前感覺好像在哪裡看過的例子:
將 UserAccount
這個介面作為 AccountSystem
以及 AccountPersonalInfo
的擴展(Extension)。因此我們來測測看以下基礎狀況。(檢驗結果如圖一)
圖一:檢驗結果,少一鍵或多一鍵都會出現警告
接下來,我們可以下一個重點:TypeScript 的型別系統與介面的主要差別(之一 XD)。
重點 1. 介面的擴展 Interface Extension / Inheritance
若我們有一系列 TypeScript 介面
I1
,I2
...In
,其中:所有的介面裡,不同的介面卻互相有重複的屬性名稱 —— 這種情形是可以接受的;然而,名稱相同之屬性,各自對應之型別不能互相衝突。若滿足以上條件,並且宣告介面
IMain
為I1
,I2
...In
的擴展:interface IMain extends I1, I2, ... In {}
則
IMain
為所有I1
,I2
, ...In
交集的結果。
以下範例展示重點裡述說的條件,如果介面各自有重複的屬性名稱,但我們分成 —— 屬性之型別會不會有衝突兩種情況來看。(檢測結果如圖二;錯誤訊息如圖三、四)
圖二:我們的介面範例中,只要出現 I2
和 I3
同時交集的狀況就會產生衝突
圖三:單純對 I2
與 I3
進行交集,結果產生衝突 —— I2
與 I3
介面中,c
屬性的型別不同
圖四:跟圖三的訊息一樣
因此讀者必須注意的是:只要多個介面要進行延伸,其中的兩個介面互不相容,就不能進行擴展的動作。
從剛剛我們得出的結論:介面可以進行延伸或擴展(Interface Extension),這裡就可以開始點出介面跟型別系統的主要差別。
介面(Interface)的意義 —— 跟規格的概念很像,可以擴充設計、組裝出更複雜的功能規格
型別(Type)的意義 —— 代表靜態的資料型態,因此型別一但被定義出來則恆為固定的狀態。儘管可以利用型態的複合(
intersection
與union
)看似達到型別擴展的感覺,然而這個行為並不叫作型別擴展,而是創造出新的靜態型別
另外,還有著名的一句話:
Code against interface, not implementation: Decouple every part of your code and compose from them, instead of short-lived implementation.
直翻就是:
“程式碼不應該直接實踐出功能(implementation),而是定義一系列的介面:必須將程式碼拆卸成一系列的小區塊,將主要功能藉由各種區塊組合起來,而非專注於直截了當的實作層面。”
為何我們必須將程式碼進行拆解動作再重新組裝出我們的功能呢?原因有以下:
筆者沒有列出所有原因,要列下去應該還會有更多。
通常一段程式也是一次只做一件事情比較好(跟單一職責原則 SRP:Single Responsibility Principle 的感覺很像)—— 換成介面的想法,通常會把大功能一次拆成好幾個介面,然後再重新組合成想要的功能,其中每個介面都代表功能的一小部分。另外,這些被拆成的小介面有些就可以被重複利用,再組成另一種功能。
貼心小提示
軟體設計裡通常提到的設計模式或概念幾乎主要都是針對 OOP 裡的類別(Class),SRP 也不例外!
原本 SRP 的定義是這樣:
SRP: Single-Responsibility Principle
"A class should have only one reason to change."很明顯,它指的是 Class,並不是函式、介面、型別、物件等等。因此這裡要澄清一件事情:“筆者只是藉由類似的軟體設計概念進行延伸!”
至於為何筆者要強調這一點呢?
我們不能夠直接把別人講的話誤解或是當成可以運用在各種層面上。如果我們直接把單一職責原則當成嚴格的定義並展開到各種領域的話,我們幾乎所有應用程式或發明就違反此原則:“電腦本身就違反單一職責原則,因為電腦不僅能夠做運算、還能夠做好多事情。” -- 這邏輯還蠻荒唐的啊~電腦生來就是要幫人類處理耗時的事情,哪會說電腦必須符合單一職責原則。(你也可以選擇按台計算機爽爽自己)
一個概念不能被 100% 強行應用在所有領域上,但只要是合理的情況下,就可以被延展到某些領域。筆者這裡即是把 SRP 的概念延展到我們在運用介面上,絕非說:“介面本身就遵照 SRP 原則”。
以下是剛剛有講到的範例:
原本把 UserAccount
設計成所有屬性都摻雜在一起,但是如果將它拆成 AccontSystem
跟 AccountPersonalInfo
—— 光是這麼做,開發者就可以推測:
AccountSystem
跟帳戶的基本運作機制有關AccountPersonalInfo
跟使用者的個資有關簡單的運用介面進行抽象化的動作就可以更明確的溝通功能主要敘述的東西。畢竟想要讓管理程式變得輕鬆些,就要讓程式寫得很像跟專案的文件ㄧ樣,一目瞭然。
記住這種對介面進行抽象化的感覺 —— 看看另一個容易造成誤會的 type
實踐出同樣跟 UserAccount
的效果,進行 Interface
與 Type
的意義上概念的比較。
這兩種分別運用型別系統與介面同時實做出 UserAccount
的效果,大致上感覺沒差(但實際功能還是有點小差別),不過意義上差別可大了!
筆者直接不果斷翻譯:
type TUserAccount =
TAccountSystem &
TAccountPersonalInfo;
被翻譯出來的意思是:“TUserAccount
的靜態資料格式為 TAccountSystem
與 TAccountPersonalInfo
的組成”。(感覺有翻沒翻都沒差)
interface IUserAccount extends
IAccountSystem,
IAccountPersonalInfo {}
被翻譯出來的意思是:“想要實作 IUserAccount
的介面時,除了必須符合 IUserAccount
本身的屬性與功能外,還必須實作出 IAccountSystem
與 IAccountPersonalInfo
的介面定義出來的屬性與功能”。
哪一個版本聽起來很像在設計功能?後者 interface
的讀法通順很多,因此才會被建議:
"Code against interface, not an implementation"
interface
單字可以類比為 TypeScript 的介面,而 implementation
單字可以類比為型別系統裡的 type
。
再者,interface
跟 type
最大差別就是今天一開始講到的:能不能夠被延展(Extensibility)。
TypeScript 介面使用 extends
進行介面延展的這個特性是根本的,就很像是數字從 0 開始數是根本的(可能會從 1 開始也說不定)—— 不會有人從 3.1415926 或 2.71828 開始數數字。
而 TypeScript 型別:不管如何,代表的意義都是指 -- 靜態的格式。『 靜態 』這兩個字對於型別系統代表的意義是根本。不管你再怎麼重組型別,如果是用 type
宣告,你都只是在定義新的型別!如果這種重組行為被歸類為延展的話 -- 相反地,那叫做『 動態 』囉。
因此,大部分針對 TypeScript 介面跟型別的比較結論是:我們可以對介面進行延展,而型別不行!
重點 2. 介面與型別的意義與特性
介面與型別各自代表的意義如下:
- 介面:規格(Spec)的概念,可以組裝、延展(使用
extends
)- 型別:靜態的資料格式,不能被延展,每一次宣告新的型別化名 —— 對型別進行複合形式的操作 —— 都是在定義新的型別,不是延展作用
今天至少開拓了型別 V.S. 介面的比較 —— 這個令筆者頭痛的議題(寫這篇文章跟打 BOSS 沒兩樣),後面我們還要介紹更多介面的功能。
最終的介面與型別的完整比較會在 Day 17. 揭曉~到時候會 Review 到今天學到的東西~